• 智能指针- shared_ptr ref_count

  • 自旋锁

  • enable_share_from_this 是什么意思,请举一个场景说明

    • 需求:在类的内部需要自身的shared_ptr而不是this裸指针
    • 场景:在类中发起一个异步操作,callback回来要保证发起操作的对象仍然有效

    使用boost库时,经常会看到这样的类:

    1
    class A : public enable_share_from_this<A>

    在什么情况下要使A继承enable_share_from_this?

    使用场合:

    当类A被shared_ptr管理,且在类A的成员函数里需要把当前类对象作为参数传给其他函数时,就需要传递一个自身的shared_ptr,我们就使类A继承enable_share_from_this,然后通过其成员函数share_from_this返回指向自身的shared_ptr。

    以上有两个疑惑:

    • 把当前类对象作为参数传给其他函数时,为什么要传递shared_ptr,直接传递this指针不行吗?

      一个裸指针传递给调用者,谁也不知道调用者会干什么,加入调用者delete了该对象,而shared_ptr此时还指向该对象

    • 直接传递shared_ptr可以吗?shared_ptr<this>

      这样会造成两个非共享的shared_ptr指向一个对象,最后造成两次析构该对象。

    本部分转载自C++ boost库—-share_from_this类的作用和实现原理

  • 侵入式与非侵入式智能指针

    非侵入式智能指针的实现完全放在智能指针模板里,模板类有一个用于保存资源类对象的指针变量,和一个用于记录资源对象使用计数的指针变量,这两个东西是所有的智能指针对象共享的,所以通过指针保存。

    侵入式的实现则分散在智能指针模板和使用智能指针模板的类中,模板类只有一个用于保存对象的指针变量,对象的计数则放在了资源类中。

    非侵入式智能指针的引用计数变量为了保证所有对象共享,需要用堆里的内存,所以需要用new。

    侵入式智能指针的引用计数变量保存在对象里,因为对象是唯一的,所以引用计数也是唯一的。

    侵入式智能指针的好处是:

    • 一个资源对象无论被多少个侵入式智能指针包含,从始至终只有一个引用计数变量,不需要在每一个使用智能指针对象的地方都new一个计数对象,这样效率比较高,使用内存也比较少,也比较安全
    • 因为引用计数变量存储在对象本身,所以在函数调用的时候可以直接传递资源对象地址,而不用担心引用计数值丢失(非侵入式智能指针对象的拷贝,必须带着智能指针模板,否则就会出现对象引用计数丢失)
  • 弱指针

    对于引用计数法实现的计数,总是避免不了循环引用的问题:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    #include <iostream>
    #include <memory>
    #include <vector>
    using namespace std;
    class ClassB;
    class ClassA
    {
    public:
    ClassA() { cout << "ClassA Constructor..." << endl; }
    ~ClassA() { cout << "ClassA Destructor..." << endl; }
    shared_ptr<ClassB> pb; // 在A中引用B
    };
    class ClassB
    {
    public:
    ClassB() { cout << "ClassB Constructor..." << endl; }
    ~ClassB() { cout << "ClassB Destructor..." << endl; }
    shared_ptr<ClassA> pa; // 在B中引用A
    };
    int main() {
    shared_ptr<ClassA> spa = make_shared<ClassA>();
    shared_ptr<ClassB> spb = make_shared<ClassB>();
    spa->pb = spb;
    spb->pa = spa;
    // 函数结束,思考一下:spa和spb会释放资源么?
    }

    上面代码的输出为:

    1
    2
    3
    ClassA Constructor...
    ClassB Constructor...
    Program ended with exit code: 0

    从上面代码中可以看出,Class A和Class B之间存在着循环引用,当main函数运行结束后,spa和spb管理的动态资源并没有得到释放,产生了内存泄漏!

    为了解决类似的问题,C++11引入了weak_ptr,来打破这种循环引用。

    weak_ptr是为了配合shared_ptr而引入的一种智能指针,它指向一个由shared_ptr管理的对象而不影响所指对象的生命周期,也就是将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。不论是否有weak_ptr指向,一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。从这个角度看,weak_ptr更像是shared_ptr的一个助手而不是智能指针。

    当我们创建一个weak_ptr时,需要用一个shared_ptr实例来初始化weak_ptr,由于是弱共享,weak_ptr的创建并不会影响shared_ptr的引用计数值。

    1
    2
    3
    4
    5
    6
    7
    int main() {
    shared_ptr<int> sp(new int(5));
    cout << "创建前sp的引用计数:" << sp.use_count() << endl; // use_count = 1
    weak_ptr<int> wp(sp);
    cout << "创建后sp的引用计数:" << sp.use_count() << endl; // use_count = 1
    }

    既然weak_ptr并不改变其所共享的shared_ptr实例的引用计数,那就可能存在weak_ptr指向的对象被释放掉这种情况。这时,我们就不能使用weak_ptr直接访问对象。那么我们如何判断weak_ptr指向对象是否存在呢?C++中提供了lock函数来实现该功能。如果对象存在,lock()函数返回一个指向共享对象的shared_ptr,否则返回一个空shared_ptr。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class A
    {
    public:
    A() : a(3) { cout << "A Constructor..." << endl; }
    ~A() { cout << "A Destructor..." << endl; }
    int a;
    };
    int main() {
    shared_ptr<A> sp(new A());
    weak_ptr<A> wp(sp);
    //sp.reset();
    if (shared_ptr<A> pa = wp.lock())
    {
    cout << pa->a << endl;
    }
    else
    {
    cout << "wp指向对象为空" << endl;
    }
    }

    weak_ptr并没有重载operator->和operator *操作符,因此不可直接通过weak_ptr使用对象,典型的用法是调用其lock函数来获得shared_ptr示例,进而访问原始对象。

    最后,我们来看看如何使用weak_ptr来改造最前面的代码,打破循环引用问题。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    class ClassB;
    class ClassA
    {
    public:
    ClassA() { cout << "ClassA Constructor..." << endl; }
    ~ClassA() { cout << "ClassA Destructor..." << endl; }
    weak_ptr<ClassB> pb; // 在A中引用B
    };
    class ClassB
    {
    public:
    ClassB() { cout << "ClassB Constructor..." << endl; }
    ~ClassB() { cout << "ClassB Destructor..." << endl; }
    weak_ptr<ClassA> pa; // 在B中引用A
    };
    int main() {
    shared_ptr<ClassA> spa = make_shared<ClassA>(); // 只进行一次内存申请
    shared_ptr<ClassB> spb = make_shared<ClassB>();
    spa->pb = spb;
    spb->pa = spa;
    // 函数结束,思考一下:spa和spb会释放资源么?
    }

    输出结果如下:

    1
    2
    3
    4
    5
    ClassA Constructor...
    ClassB Constructor...
    ClassA Destructor...
    ClassB Destructor...
    Program ended with exit code: 0

    从运行结果可以看到spa和spb指向的对象都得到释放!

    本小节转载自文章【C++11新特性】 C++11智能指针之weak_ptr

  • vector的扩容倍数

  • class作为map/unordered_map的key分别需要实现什么?前者是<后者是==和hash

  • C++程序如何和其他语言交互

  • vector的插入异常安全是怎么实现的?

  • deque如何实现O(1)下标查找

  • shared_ptr

    shared_ptr是引用计数型智能指针,采用的是在堆上放个计数值的方法。

    • 空的shared_ptr

      1
      shared_ptr<T> ptr;
    • 使用new创建

    1
    shared_ptr<T> ptr(new T());
    • 使用复制构造函数后者重载=构造

      1
      2
      shared_ptr<T> ptr1(new T());
      shared_ptr<T> ptr2(ptr1);
      1
      2
      3
      shared_ptr<T> ptr1(new T());
      shared_ptr<T> ptr2;
      ptr2 = ptr1;
  • 当表达式中存在有符号数和无符号数所有的操作数都要转换为无符号数,所以从这个意义上讲,无符号数的运算优先级要高于有符号数。

    有符号数转换为无符号数,负数转换为大的正数,相当于在原值上加上2的n次方,而正数保持不变。

    无符号数转换为有符号数,对于小的数将保持原值,对于大的数将转换为负数,相当于原值减去2的n次方。

  • 用const修饰的符号常量的区别:

    const位于*的左边,表示被指物是常量,const位于*的右边,表示指针自身是常量(常量指针)。

  • 在C++中只使用const常量而不使用宏常量。

  • 用运算符sizeof可以计算出数组的容量(字节数),sizeof(p),p为指针,得到的是一个指针变量的字节数,而不是p所指的内存容量,C/C++语言没有办法知道指针所指的内存容量,除非在申请内存时记住它。

  • 为了节省内存,C/C++语言将常量字符串放到单独的一个内存区域,当几个指针赋值给相同的常量字符串时,它们实际上会指向相同的内存地址,但用常量内存初始化数组时,情况有些不同:

    1
    2
    3
    4
    char str1[] = "helloworld";
    char str2[] = "helloworld";
    char* str3 = "helloworld";
    char* str4 = "helloworld"

    其中,str1和str2会为它们分配两个长度相同的空间,并把”helloworld”的内容复制到数组中去,这是两个初始地址不同的数组(在栈中分配内存)。

    str3和str4是两个指针,我们无需为它们分配内存以存储字符串的内容,而只需要将它们指向”helloworld”在内存中的地址即可,由于”helloworld”是常量字符串,它在内存中只有一个拷贝,因此str3和str4指向的是同一个地址。

  • sizeof是C语言的一个单目操作符,它并不是函数,操作数可以是一个表达式或类型名,数据类型必须用括号括住:sizeof(int),变量名可以不用括号括住。

    1
    2
    3
    4
    5
    6
    int a[50]; // sizeof(a) = 200;
    int* a = new int[50]; // sizeof(a) = 4;
    class Test {int a; static double c}; // sizeof(Test) = 4;
    Test* s; // sizeof(s) = 4;
    class Test {}; // sizeof(Test) = 1;
    int func(char s[5]); // sizeof(s) = 4;
    • 数组类型:其结果是数组的总字节数
    • 指向数组的指针:其结果是该指针的字节数
    • 函数中的数组形参或函数类型的形参:其结果是指针的字节数
    • 类中的静态成员不会结果产生影响,因为静态变量的存储位置与结构或者类的实例地址无关
    • 没有成员变量的类的大小为1,因为必须保证类的每一个实例在内存中都有一个唯一的地址
    • 有虚函数的类都会建立一张虚函数表,表中存放的是虚函数的函数指针,这个表的地址存放在类中,所以不管有几个虚函数,都只占据一个指针大小。
  • 在32位系统中:

    1
    2
    3
    4
    5
    char arr[] = {4, 3, 9, 2, 0, 1, 5};
    char* str = arr;
    sizeof(arr) = 8;
    sizeof(str) = 4;
    strlen(str) = 5;
    • sizeof(arr):arr是一个数组,sizeof(arr)表示数组所占用的字节数
    • sizeof(str):str为一个指针,sizeof(str)表示指针所占用的字节数
    • strlen(str):strlen函数求取字符串长度以ASCII值为0为止
  • dynamic_cast:主要用于执行安全向下转型,也就是用来决定某对象是否归属继承体系中的某个类型,主要用于多态类之间的转换。

    • 安全的基类和子类之间进行转换
    • 必须要有虚函数
    • 相同基类不同子类之间的交叉转换,但结果是NULL
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class Base {
    public:
    int m_iNum;
    virtual void fun() {}; // 基类必须有虚函数,保持多态性才能使用dynamic_cast
    };
    class Derive: public Base {
    public:
    char*m_szName[100];
    void bar(){};
    };
    Base* pb =new Derive();
    Derive *pd1 = static_cast<Derive *>(pb); //子类->父类,静态类型转换,正确但不推荐
    Derive *pd2 = dynamic_cast<Derive *>(pb); //子类->父类,动态类型转换,正确
    Base* pb2 =new Base();
    Derive *pd21 = static_cast<Derive *>(pb2); //父类->子类,静态类型转换,危险!访问子类m_szName成员越界
    Derive *pd22 = dynamic_cast<Derive *>(pb2); //父类->子类,动态类型转换,安全的。结果是NULL
  • const_cast:能够改变表达式的常量属性:

    1
    2
    3
    4
    const char c = 'a';
    const char* pc = &c;
    char* cp = const_cast<char*>(pc);
    *cp = 'c';
  • reinterpret_cast:常用于函数指针之间的转换:

    1
    2
    3
    4
    5
    int doSomething() {return 0;}
    typedef void(*FuncPtr)();
    FuncPtr funcPtrArray[10];
    funcPtrArray[0] = &doSomething; //编译错误,类型不匹配
    funcPtrArray[0] = reinterpret_cast<FuncPtr>(&doSomething); // 不同函数指针类型之间进行转换
  • static_cast:任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_cast

  • 静态成员函数不能为虚函数,静态成员函数是以类为单位的函数,与具体对象无关,虚函数是与对象动态绑定的

  • 内联函数不能成为虚函数,内联函数需要在编译阶段展开,而虚函数是在运行时动态绑定的,编译时无法展开

  • 虚拟继承是多重继承中特有的概念,虚拟继承是为解决多重继承而出现的,如类D继承自类B1、B2,而类B1、B2都继承自类A,因此在类D中两次出现类A中的变量和函数,为了节省内存空间,可以将B1和B2对A的继承定义为虚拟继承,而A就成了虚拟基类。

  • Linux进程地址空间布局:

    • 代码段(.text):用来存放可执行文件的机器指令,存放在只读区域,以防止被修改
    • 只读数据段(.rodata):用来存放常量,存放在只读区域,如字符串常量、全局const常量
    • 可读写数据段(.data):用来存放可执行文件中已初始化全局变量,即静态分配的变量和全局变量
    • BSS段(.bss):未初始化的全局变量和局部静态变量一般放在.bss段里,以节省内存空间
    • 堆:用来容纳应用程序动态分配的内存区域。当程序使用malloc或new分配内存时,得到的内存来自堆。堆通常位于栈的下方。
    • 用于维护函数调用的上下文。栈通常分配在用户空间的最高地址处分配。
    • 动态链接库映射区:如果程序调用了动态链接库,则会有这一部分。该区域是用于映射装载的动态链接库。
    • 保留区:内存中受到保护而禁止访问的内存区域。
  • 栈:栈是向低地址扩展的数据结构,是一块连续的内存区域,栈顶的地址和栈的最大容量是系统预先规定好的,栈的大小为1M,如果申请的空间超过栈的剩余空间时,将提示overflow

  • 堆:堆是向高地址扩展的数据结构,是不连续的内存区域

  • 智能指针的设计思想是将基本类型指针封装为类对象指针(这个类肯定是个模板,以适应不同基本类型的需求),并在析构函数里编写delete语句删除指针指向的内存空间

  • shared_ptr采用引用计数的智能指针,主要用于将一个指针分配给多个所有者的情况,其大小为两个指针:一个用于对象,另一个用于包含引用计数的共享控制块。

  • 在C++对象模型中,非静态数据成员被配置于每一个类的对象之中,静态数据成员则被存放在所有的类对象之外,静态及非静态成员函数也被放在类对象之外。